tests: --sniffer-iface integrates sniff_air into per-cell matrix flow#41
Merged
Conversation
Wires the radiotap verifier from #40 (tests/sniff_air.py) into regress.py so it runs alongside every cell in default / --full-matrix / --encoding-matrix modes. Optional and opt-in — defaults preserved. ## Usage sudo python3 tests/regress.py --encoding-matrix \ --tx-pid 0x8813 --rx-pid 0x0120 --channel 100 \ --vm-name devourer-testrig --vm-ssh <user>@<VM-IP> \ --sniffer-iface wlan0mon Per-cell output gains a `↪ sniffer: N frames — <encoding>=N, ...` line under the existing hit-count line. Reports what actually flew on-air, which the matrix itself can't see — closes the open question from #40 ("did the kernel-TX path actually emit LDPC, or strip the flag?"). Intended for an AR9271. The chipset speaks vanilla radiotap without driver-side flag filtering, which makes it a reliable on-air observer of what other adapters emit. ## Implementation - `_spawn_sniffer(iface, channel, pcap_path)`: sets iface to monitor mode on `channel`, runs `tcpdump -w pcap -U -nn 'ether src CANONICAL_SA'`. Always host-local; sniffer adapters don't move into the VM via virsh USB passthrough. - `_summarise_sniffer_pcap(pcap_path)`: imports sniff_air at runtime (next to regress.py), reuses `_read_pcap_frames` + `_parse_radiotap` + `_frame_sa` + `CANONICAL_SA` to bucket captured frames by encoding kind / MCS / LDPC / STBC / BW. Returns a one-line summary string suitable for the cell's `notes` field. - `run_cell(sniffer_iface=...)`: spawns sniffer between RX and TX stages so the full TX window gets captured. Sniffer failures are observational — never fail the cell on sniffer issues. - `run_matrix`, `run_full_matrix`, `run_encoding_matrix`: pass `sniffer_iface` through to `run_cell`. When the sniffer is active and `r.notes` is set, print an extra `↪ <notes>` line per cell. - `--sniffer-iface IFACE` CLI flag + `DEVOURER_SNIFFER_IFACE` env equivalent in `main()`. ## Validation Code path is dormant when `--sniffer-iface` is not set — defaults preserved, prior runs unaffected. Hardware validation requires an AR9271 plugged in and is left as the next-up follow-up; not blocking because the dormant path is verified by all existing CI / matrix runs on master (which the immediately preceding PR #40 validated end-to-end). The sniffer parser itself was unit-tested in #40: every combo `inject_beacon.build_beacon` emits round-trips back through `sniff_air._parse_radiotap` with byte-identical decoded fields. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
josephnef
added a commit
that referenced
this pull request
May 26, 2026
…trix (#46) Two bugs in the sniffer integration from #40 / #41 that together hid the real finding about what kernel TX actually emits on-air. Fixing them surfaces a definitive answer to the open LDPC question. ## Bug 1: `_parse_radiotap` returned None on every real-world frame The parser only iterated `it_present[0]` and bailed when it hit a bit not in `_RT_FIELDS`. Every captured frame from `ath9k_htc` has presence `0xa000402f` with bits `[0,1,2,3,5,14,29,31]` — bits 29 (`RADIOTAP_NS` marker) and 31 (`EXT` — continuation word) are control bits, not data fields. The parser hit bit 29 on every frame → `408 parse-errors / 408 frames` on the first AR9271 sniffer run. Fix: iterate bits across ALL presence words; recognise the three control bits (29 RADIOTAP_NS, 30 VENDOR_NS, 31 EXT) and skip them with the right data consumption (6 bytes for VENDOR_NS, none for the others); when hitting an unknown radiotap field, return the parsed-so-far dict rather than discarding the whole frame. Round-trip against `inject_beacon.build_beacon` for every `(HT, VHT) × (BCC, LDPC, STBC)` combo: byte-identical decoded fields. Real AR9271-captured beacon: parses cleanly as `kind=legacy`. ## Bug 2: cell pcap filename collision in `--encoding-matrix` `cell_id = f"tx-{tx_side}_rx-{rx_side}"` doesn't include the encoding label, so six encoding cells per driver-mode wrote to the same `/tmp/devourer-regress-*/tx-{tx}_rx-{rx}.sniffer.pcap`. The last cell overwrote the first five — so `--keep-logs` retained only the LAST encoding combo per mode (typically VHT-LDPC, where AR9271 captures 0 frames since it's n-only). Made post-hoc debugging impossible. Fix: optional `cell_tag` parameter on `run_cell`, set by `run_encoding_matrix` to the sanitised encoding label (`ht-bcc`, `ht-ldpc`, `vht-ldpcstbc`, …). Other matrix modes (`run_matrix`, `run_full_matrix`) leave it empty — they only run one cell per `(tx_side, rx_side)` pair. ## What this surfaces Encoding matrix re-run on the rig (8814 TX → 8821 RX, ch6, AR9271 sniffer): | Mode | Encoding requested | Sniffer decoded | |---|---|---| | k/k | HT-BCC | `HT MCS1 BCC 20MHz STBC=0` (412) | | k/k | HT-LDPC | `HT MCS1 **BCC** 20MHz STBC=0` (386) | | k/k | HT-STBC=1 | `HT MCS1 BCC 20MHz **STBC=0**` (418) | | k/k | HT-LDPC+STBC=1 | `HT MCS1 **BCC** **STBC=0**` (422) | | k/k | VHT-BCC | 0 frames (AR9271 is n-only) | | k/k | VHT-LDPC | 0 frames | Same in the `k/d` row. **`aircrack-ng/88XXau` (or mac80211 in its TX path) strips the radiotap LDPC bit and STBC stream count.** MCS index (1) and the HT-vs-VHT distinction DO survive. So every "LDPC" kernel-TX cell in #40 and #41 was actually emitting BCC on-air — the flat `k/d` row in those PRs' results never disproved @RomanLut's 8821AU LDPC-RX-no claim, it just meant we never tested the chip with an actual LDPC frame. VHT cells show 0 to AR9271 (n-only) but >0 to the 8821 RX (AC chip), so the HT/VHT distinction IS honoured. We still can't see whether mac80211 strips the VHT-LDPC bit specifically — would need an AC-capable sniffer (or to capture on the 8821 RX itself in monitor mode). ## Implications - The `--encoding-matrix` mode is most useful for chip-side asymmetries reachable through MCS-index and HT-vs-VHT; it can't validate LDPC- or STBC-specific RX behaviour through the kernel TX path as currently wired. - A proper LDPC-RX validation needs a userspace TX path that writes the radiotap directly to the chip — devourer's txdemo does this (`DEVOURER_TX_LDPC=1` env var is ground-truth). 8814 TX being broken on master is the blocker for using this from the d/k cells. - The AR9271 sniffer integration is functional; the data it produces is now reliable. Future hotplug / encoding investigations can rely on it. ## Test plan - [x] Parser round-trips every encoding combo `inject_beacon.build_beacon` emits - [x] Parser handles real AR9271 capture (multi-word presence, bits 29/31 set) - [x] `--encoding-matrix` produces distinct pcap-per-cell with `--keep-logs` - [x] Re-ran end-to-end on the rig — all 24 cells emit reliable sniffer summaries - [x] CI builds — no C++ changes here, Python-only 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the last of #39's three follow-ups: wires the radiotap verifier from #40 (
tests/sniff_air.py) intotests/regress.pyso it runs alongside every cell. Optional and opt-in —--sniffer-ifacedefaults to off, all prior modes unchanged.What this is
sudo python3 tests/regress.py --encoding-matrix \ --tx-pid 0x8813 --rx-pid 0x0120 --channel 100 \ --vm-name devourer-testrig --vm-ssh <user>@<VM-IP> \ --sniffer-iface wlan0monPer-cell output gains a
↪ sniffer: N frames — <encoding>=N, ...line under the existing hit-count line. Reports what actually flew on-air for each cell — closes the open question from #40 ("did the kernel-TX path actually emit LDPC, or strip the flag?").Intended for an AR9271: vanilla radiotap, no driver-side filtering on what the cell injects. The chipset is widely used as a sniffer for exactly this reason.
Implementation
_spawn_sniffer(iface, channel, pcap_path)tcpdump -w pcap -U -nn 'ether src CANONICAL_SA'. Always host-local; sniffer never moved into the VM via USB passthrough._summarise_sniffer_pcap(pcap_path)_read_pcap_frames+_parse_radiotap+_frame_sa+CANONICAL_SAto bucket captured frames. Returns a one-line summary for the cell'snotesfield.run_cell(sniffer_iface=...)run_matrix/run_full_matrix/run_encoding_matrixsniffer_ifacethrough torun_cell. When active +r.notesis set, print an extra↪ <notes>line per cell.--sniffer-iface IFACEDEVOURER_SNIFFER_IFACEenv equivalent.Validation
The dormant path (
sniffer_iface=None) preserves the exact prior behaviour of every matrix mode — only structural change inrun_cellis initializingsniffer_proc=Noneand a no-op cleanup if it stays None.Active-path validation requires an AR9271 plugged in, which isn't in the rig today. CI matrix builds will confirm the code paths compile / import. Functional end-to-end pending hardware.
The sniffer parser itself was unit-tested in #40: every combo
inject_beacon.build_beaconemits round-trips back throughsniff_air._parse_radiotapwith byte-identical decoded fields.What this PR doesn't touch
ENCODING_COMBOSunchanged from tests: VHT encoding combos + sniff_air.py radiotap verifier #40.Test plan
--helplists--sniffer-iface--sniffer-iface IFACEwith an AR9271 plugged in: verify per-cell↪ sniffer:lines appear and decode reasonably--sniffer-ifacerunning alongside--encoding-matrixshows different encoding distributions for--ldpcvs default cells (the actual goal — proves whether mac80211 / 88XXau emits the LDPC bit on-air)🤖 Generated with Claude Code